package com.fire.mongo.lessons;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import com.fire.mongo.lessons.utils.ActorBasic;
import com.fire.mongo.lessons.utils.ActorCodec;
import com.fire.mongo.lessons.utils.ActorWithStringId;
import com.fire.mongo.lessons.utils.StringObjectIdCodec;
import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.model.Filters;
import org.bson.Document;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.codecs.pojo.ClassModel;
import org.bson.codecs.pojo.ClassModelBuilder;
import org.bson.codecs.pojo.PojoCodecProvider;
import org.bson.codecs.pojo.PropertyModelBuilder;
import org.bson.conversions.Bson;
import org.bson.types.ObjectId;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import org.springframework.boot.test.context.SpringBootTest;

import static org.bson.codecs.configuration.CodecRegistries.fromCodecs;
import static org.bson.codecs.configuration.CodecRegistries.fromProviders;
import static org.bson.codecs.configuration.CodecRegistries.fromRegistries;

/**
 * @date 2022/5/4
 * @author zwd
 * @email zwd@hhh.com
 *
 * 在本节中，我们将使用Java Driver去解析document
 * */
@SuppressWarnings("unchecked")
@SpringBootTest
public class UsingPojoLesson extends AbstractLesson {
	private ObjectId actor1Id;
	private ObjectId actor2Id;

	public UsingPojoLesson() {
		super();
	}

	/**
	 * 创建一个基本 ActorBasic
	 */
	@Before
	public void setUp() throws ParseException {
		MongoCollection<Document> actors = testDb.getCollection("actors");

		/*
		现在我们创建2个基本的document，并填充他们的属性
		 */
		Document actorDocument1 = new Document();
		actorDocument1.append("name", "Bruce Campbell");
		actorDocument1.append("date_of_birth", new SimpleDateFormat("yyyy-MM" + "-dd").parse("1981-06-09"));
		actorDocument1.append("awards", Collections.EMPTY_LIST);
		actorDocument1.append("num_movies", 127);

		Document actorDocument2 = new Document();
		actorDocument2.append("name", "Natalie Portman");
		actorDocument2.append(
				"date_of_birth", new SimpleDateFormat("yyyy-MM" + "-dd").parse("1981-06-09"));
		actorDocument2.append("awards", Collections.EMPTY_LIST);
		actorDocument2.append("num_movies", 63);

		/*
		现在我们创建2个 actor document ，我们使用insertMany方法将其插入到 actors 集合中
		 */

		ArrayList<Document> actorsDocument = new ArrayList<>();
		actorsDocument.add(actorDocument1);
		actorsDocument.add(actorDocument2);
		actors.insertMany(actorsDocument);

		actor1Id = actorDocument1.getObjectId("_id");
		actor2Id = actorDocument2.getObjectId("_id");
	}

	/**
	 * pojoCodecRegistry 是 Codecs 的工厂，其中 Codecs 是一个抽象类，并且它可以将Bson数据转化为
	 * 其它类型。 Codecs 可以是默认的，也可以是自定义的，现在我们使用默认的 Codecs 来尝试
	 * PojoCodecProvider 提供了一个 builder 它可以配置POJO支持的方式和内容。
	 * builder 允许注册类和包，并提供了一些有用的配置，帮助处理其中的POJO
	 *
	 * 这里我们实例一个Codecs，并使用它默认的配置
	 */
	@Test
	public void testReadUsingPojo(){
		CodecRegistry pojoCodecRegistry = fromRegistries(MongoClientSettings.getDefaultCodecRegistry(),
				fromProviders(PojoCodecProvider.builder().automatic(true).build()));

		MongoCollection<ActorBasic> actors = testDb.getCollection("actors", ActorBasic.class)
				.withCodecRegistry(pojoCodecRegistry);
		// 获取它的id
		Bson queryFilter = new Document("_id", actor1Id);

		ActorBasic pojoActor = actors.find(queryFilter).iterator().tryNext();

		Assert.assertNotNull(pojoActor);

		Assert.assertTrue(pojoActor instanceof ActorBasic);

		MongoCollection<Document> actorsCollectionDoc = testDb.getCollection("actors");

		Document docActor = actorsCollectionDoc.find(queryFilter).iterator().tryNext();

		Assert.assertEquals(docActor.getObjectId("_id"), pojoActor.getId());
		Assert.assertEquals(docActor.get("name"), pojoActor.getName());
		Assert.assertEquals(docActor.get("date_of_birth"), pojoActor.getDateOfBirth());
		Assert.assertEquals(docActor.get("awards"), pojoActor.getAwards());
		Assert.assertEquals(docActor.get("num_movies"), pojoActor.getNumMovies());

	}

	/*
	第一次运算上述方法会发生错误，主要是数据名和类型在类和数据库之间存在差异，导致注入失败。

	 */
	public ActorWithStringId fromDocument(Document doc) {
		ActorWithStringId actor = new ActorWithStringId(); // create an empty ActorWithStringId object
		actor.setId(doc.getObjectId("_id").toHexString()); // convert to String
		actor.setName(doc.getString("name"));
		actor.setDateOfBirth(doc.getDate("date_of_birth"));
		actor.setAwards((List<Document>) doc.get("awards"));
		actor.setNumMovies(doc.getInteger("num_movies"));
		return actor;
	}

	@Test
	public void testMappingDocumentsToActorClass() {
		MongoCollection<Document> actors = testDb.getCollection("actors");

		Document actorDocument = actors.find(new Document("_id", actor1Id)).iterator().tryNext();

		Assert.assertNotNull("We should be able to find the actor", actorDocument);

		ActorWithStringId actor = fromDocument(actorDocument);
		Assert.assertNotNull(actor);
		Assert.assertEquals(actor.getId(), actorDocument.get("_id").toString());
		Assert.assertEquals(actor.getName(), actorDocument.getString("name"));
		Assert.assertEquals(actor.getDateOfBirth(), actorDocument.getDate("date_of_birth"));
		Assert.assertEquals(actor.getAwards(), actorDocument.get("awards"));
		Assert.assertEquals(actor.getNumMovies(), actorDocument.get("num_movies"));
	}


  /*
   插入数据，在这过程中需要将java 对象转化为 document。但java对象中包含String类型的id，这
   和db中的ObjectID并不匹配，为了将id插入，我们需要自定义 codec
   */

	@Test
	public void testReadObjectsWithCustomCodec() {

		// 自定义codec 实现解码和编码的方法
		ActorCodec actorCodec = new ActorCodec();

		CodecRegistry codecRegistry =
				fromRegistries(MongoClientSettings.getDefaultCodecRegistry(), fromCodecs(actorCodec));

		Bson queryFilter = new Document("_id", actor1Id);
		MongoCollection<ActorWithStringId> customCodecActors =
				testDb.getCollection("actors", ActorWithStringId.class).withCodecRegistry(codecRegistry);

		ActorWithStringId actor = customCodecActors.find(Filters.eq("_id", actor1Id)).first();

		Assert.assertEquals(actor1Id.toHexString(), actor.getId());

		MongoCollection<Document> actorsCollectionDoc = testDb.getCollection("actors");

		ActorWithStringId mappedActor =
				fromDocument(actorsCollectionDoc.find(queryFilter).iterator().tryNext());

		Assert.assertNotNull(actor);
		Assert.assertNotNull(mappedActor);
		Assert.assertEquals(mappedActor.getId(), actor.getId());
		Assert.assertEquals(mappedActor.getName(), actor.getName());
		Assert.assertEquals(mappedActor.getDateOfBirth(), actor.getDateOfBirth());
		Assert.assertEquals(mappedActor.getAwards(), actor.getAwards());
		Assert.assertEquals(mappedActor.getNumMovies(), actor.getNumMovies());
		// looks like they do, which is great news.
	}

	/*
	 * 现在的问题是使用自定义的 codec 的话，我们需要自己些 pojo 到 document 的映射，它不允许
	 * 有类型的改变。接下来使用 CustomCodec 来实现该过程
	 */

	@Test
	public void testWriteObjectsWithCustomCodec() {
		ActorCodec actorCodec = new ActorCodec();
		// we now create a codecRegistry with the custom Codec
		CodecRegistry codecRegistry =
				fromRegistries(MongoClientSettings.getDefaultCodecRegistry(), fromCodecs(actorCodec));
		// and get the "actors" collection using our new Registry
		MongoCollection<ActorWithStringId> customCodecActors =
				testDb.getCollection("actors", ActorWithStringId.class).withCodecRegistry(codecRegistry);
		// we can now create a new actor and insert it directly into our
		// collection with all required features present.
		ActorWithStringId actorNew = new ActorWithStringId();
		actorNew.setNumMovies(2);
		actorNew.setName("Norberto");
		customCodecActors.insertOne(actorNew);
		Assert.assertNotNull(actorNew.getId());
	}

	/*
	 * 使用 CodecRegistry 定义需要特别处理的字段，
	 */

	@Test
	public void testReadObjectWithCustomFieldCodec() {
		// select a class that will be used as our POJO
		ClassModelBuilder<ActorWithStringId> classModelBuilder =
				ClassModel.builder(ActorWithStringId.class);
		// get the property that needs type conversion
		PropertyModelBuilder<String> idPropertyModelBuilder =
				(PropertyModelBuilder<String>) classModelBuilder.getProperty("id");
		// apply type conversion to the property of interest
		// StringObjectIdCodec describes specifically how to encode and decode
		// the ObjectId into a String and vice-versa.
		idPropertyModelBuilder.codec(new StringObjectIdCodec());
		// use the default CodecRegistry, with the changes implemented above
		// through registering the classModelBuilder with the PojoCodecProvider
		CodecRegistry stringIdCodecRegistry =
				fromRegistries(
						MongoClientSettings.getDefaultCodecRegistry(),
						fromProviders(
								PojoCodecProvider.builder()
										.register(classModelBuilder.build())
										.automatic(true)
										.build()));
		// we're done! Lets test if it worked! let us get the actors collection
		MongoCollection<ActorWithStringId> actors =
				testDb
						.getCollection("actors", ActorWithStringId.class)
						.withCodecRegistry(stringIdCodecRegistry);
		// build a search query
		Bson queryFilter = new Document("_id", actor1Id);
		// find the actor that we want to test for
		ActorWithStringId stringIdActor = actors.find(queryFilter).iterator().tryNext();
		// make sure we have a non empty object
		Assert.assertNotNull(stringIdActor);
		// make sure we have an instance of ActorWithStringId
		Assert.assertTrue(stringIdActor instanceof ActorWithStringId);
		// the id values match
		Assert.assertEquals(actor1Id.toHexString(), stringIdActor.getId());
		// now we can compare values across two different methods like we did
		// in our earlier test cases
		MongoCollection<Document> actorsCollectionDoc = testDb.getCollection("actors");
		ActorWithStringId mappedActor =
				fromDocument(actorsCollectionDoc.find(queryFilter).iterator().tryNext());

		Assert.assertNotNull(mappedActor);
		Assert.assertEquals(mappedActor.getId(), stringIdActor.getId());
		Assert.assertEquals(mappedActor.getName(), stringIdActor.getName());
		Assert.assertEquals(mappedActor.getDateOfBirth(), stringIdActor.getDateOfBirth());
		Assert.assertEquals(mappedActor.getAwards(), stringIdActor.getAwards());
		Assert.assertEquals(mappedActor.getNumMovies(), stringIdActor.getNumMovies());
		// looks like they do, which is great news.

	}

	@After
	public void tearDown() {
		testDb.getCollection("actors").drop();
		actor1Id = null;
		actor2Id = null;
	}

	/* Let us now recap the advantages and disadvantages of the methods
	 * discussed in today's lesson
	 *
	 * 1. The Pojo implementation is much cleaner, it outsources translation
	 * between BSON and object to a custom codec which makes it easier to
	 * manage and maintain the code.
	 *
	 * 2. In either approach the field names don't have to match the object
	 * attribute name. In case of the custom Codec this is easy to
	 * accommodate in the code, via the @BsonProperty annotation only for the
	 * fields that don't correspond to the attribute name one-to-one.
	 *
	 * 3. To map the Doc to an object manually you have to make use of
	 * getters and setters, while using a POJO allows for automatic
	 * introspection (a.k.a automatic process of analyzing a bean's design
	 * patterns to reveal the bean's properties, events, and methods).
	 *
	 * 4. Handling of generics and subdocuments is cleaner and easier to
	 * maintain with POJO, utilizing the Custom Codec or the field
	 * customization approach instead of writing separate
	 * methods to traverse sub-documents with getters and setters.
	 *
	 * 5. Finally, when writing a Document object to the DB, the document _id is
	 * automatically generated and can be accessed in the Document object for
	 * the subsequent use in the mapping method.
	 *
	 * This is the end of your Using Pojo Lesson.
	 *
	 * Enjoy your the next one!
	 *
	 *
	 */
}
